Отдел аналитики Яндекс.Афиши. Задача: помочь маркетологам оптимизировать маркетинговые затраты. В распоряжении есть данные от Яндекс.Афиши с июня 2017 по конец мая 2018 года.
Изучим:
План на проект </i>
Шаг 1. Загрузка данных </i>
Шаг 2. Подготовка данных к анализу </i>
Шаг 3. Составление отчётов и расчет метрик </i>
Продукт
Сколько людей пользуются в день, неделю, месяц?
Сколько сессий в день?
Сколько длится одна сессия?
Как часто люди возвращаются?
Продажи
Когда люди начинают покупать?
Сколько раз покупают за период?
Какой средний чек?
Сколько денег приносят? (LTV)
Маркетинг
Сколько денег потратили? Всего / на каждый источник / по времени
Сколько стоило привлечение одного покупателя из каждого источника?(САС)
На сколько окупились расходы? (ROI)
Шаг 4. Общий вывод: </i> рекомендация маркетологам, куда и сколько им стоит вкладывать денег? Какие источники/платформы перспективны? На какие метрики вы ориентируетесь? Почему? Какие выводы вы сделали, узнав значение метрик?
import pandas as pd
from matplotlib import pyplot as plt
import numpy as np
import plotly.express as px
from scipy import stats as st
import seaborn as sns
from datetime import datetime
import plotly.graph_objects as go
from matplotlib.axes._axes import _log as matplotlib_axes_logger
matplotlib_axes_logger.setLevel('ERROR')
visits = pd.read_csv('/Users/admin2/Desktop/2020/yandx/data/6/visits_log.csv')
# let's rename the columns
visits = visits.rename(columns={"Device": "device", "End Ts": "ssn_end", \
"Source Id": "source_id", "Start Ts":"ssn_start","Uid":"user_id"})
orders = pd.read_csv('/Users/admin2/Desktop/2020/yandx/data/6/orders_log.csv')
# let's rename the columns
orders = orders.rename(columns={"Buy Ts": "order_date", "Revenue": "revenue", "Uid": "user_id"})
costs = pd.read_csv('/Users/admin2/Desktop/2020/yandx/data/6/costs.csv')
# let's rename the columns
costs = costs.rename(columns={"dt": "date"})
Были изменены названия некоторых столбцов. Также, пропусков в пункте 1 не было выявлено, но типы данных необходимо поправить: заменить тип данных для дат, чтобы их можно было сравнивать между собой и для упрощения работы.
Таблица visits (лог сервера с информацией о посещениях сайта):
user_id — уникальный идентификатор пользователя
device — категория устройства пользователя
ssn_start — дата и время начала сессии
ssn_end — дата и время окончания сессии
source_id — идентификатор рекламного источника, из которого пришел пользователь
Таблица orders (информация о заказах):
user_id — уникальный id пользователя, который сделал заказ
order_date — дата и время заказа
revenue — выручка Яндекс.Афиши с этого заказа
Таблица costs (информация о затратах на маркетинг):
source_id — идентификатор рекламного источника
date — дата
costs — затраты на этот рекламный источник в этот день
# to datetime data type
for date_column_name in ["ssn_end", "ssn_start"]:
visits[date_column_name] = pd.to_datetime(visits[date_column_name])
# checking duplicated data
visits[visits.duplicated(keep = False)].sort_values(by = visits.columns.values.tolist())
# to datetime data type
orders['order_date'] = pd.to_datetime(orders['order_date'])
# checking duplicated data
orders[orders.duplicated(keep = False)].sort_values(by = orders.columns.values.tolist())
# to datetime data type
costs['date'] = pd.to_datetime(costs['date'])
# checking duplicated data
costs[costs.duplicated(keep = False)].sort_values(by = costs.columns.values.tolist())
заменили тип данных для дат и проверили таблицы на дубликаты.
Выбросы могут быть в orders['revenue'] (выручка Яндекс.Афиши с определенного заказа) и в costs['costs'] (затраты на рекламный источник в определенный день). Проверим.
orders['revenue'].describe()
fig = px.histogram(orders, x="revenue", marginal="box", title = 'Распределение revenue',\
labels={
"count": "Количество",
"revenue": "Выручка Яндекс.Афиши с заказа, у.е."
})
fig.show()
orders.query('revenue > 2000')
Посмотрим подробнее на распределение выручки.
fig = px.histogram(orders.query('revenue <=20'), x="revenue", marginal="box", title = 'Распределение revenue',\
labels={
"count": "Количество",
"revenue": "Выручка Яндекс.Афиши с заказа, у.е."
})
fig.show()
print('Доля выбросов:', orders.query('revenue >= 15').shape[0] / orders.shape[0]*100, '%')
orders_cut = orders.query('revenue <= 15')
orders.revenue.describe()
costs['costs'].describe()
fig = px.histogram(costs, x="costs", marginal="box", title = 'Распределение costs',\
labels={
"count": "Количество",
"costs": "Стоимость рекламного источника, у.е."
})
fig.show()
print('Доля выбросов:', costs.query('costs > 400').shape[0] / costs.shape[0]*100)
print('Доля выбросов:', costs.query('costs > 500').shape[0] / costs.shape[0]*100)
costs_cut = costs.query('costs <= 500')
costs = costs.query('costs <= 1000')
В расходах и доходах часто встречаются выбросы. Так как нас интересуют выводы для среднестатистических пользователей, мы подчистили данные от аномальных значений в таблицах orders & costs. Удаляли не более 5% данных.
3.1.1 DAU, WAU, MAU
3.1.2 ASL
3.1.3 Sticky Factor
3.1.4 Customer Retention Rate
3.2.1 Когда люди начинают покупать?
3.2.2 Когортный анализ
3.2.3 Анализ абсолютных показателей
3.2.4 Анализ относительных показателей
3.2.5 Изменение среднего чека
3.2.6 LTV
3.3.1 CAC
3.3.2 ROI
3.3.3 Окупаемость источников по устройствам
3.3.4 Воронки
Сколько людей пользуются в день, неделю, месяц? Сколько сессий в день? Сколько длится одна сессия? Как часто люди возвращаются?
Таблица visits (лог сервера с информацией о посещениях сайта): user_id — уникальный идентификатор пользователя device — категория устройства пользователя ssn_start — дата и время начала сессии ssn_end — дата и время окончания сессии source_id — идентификатор рекламного источника, из которого пришел пользователь
Для ответа на эти вопросы воспользуемся следующими метриками.
На активных пользователей смотрят в трёх измерениях:
DAU (от англ. daily active users) — количество уникальных пользователей в день;
WAU (от англ. weekly active users) — количество уникальных пользователей в неделю;
MAU (от англ. monthly active users) — количество уникальных пользователей в месяц.
Посмотрим на среднюю продолжительность сессии (ASL) и узнаем сколько длится сессия пользователя в среднем
Sticky factor (sticky factor = DAU/WAU или sticky factor = DAU/MAU). По этой метрике можно уыидеть регулярность использования вашего приложения — то, как часто пользователи к нему обращаются.
Чтобы узнать как часто люди возвращаются нужно вычислить Customer Retention Rate.
# Чтобы посчитать недельное и месячное использование,
# выделим в отдельные столбцы
# год, месяц и неделю, полную дату.
visits['session_year'] = visits['ssn_start'].dt.year
visits['session_month'] = visits['ssn_start'].dt.month
visits['session_week'] = visits['ssn_start'].dt.week
visits['session_date'] = visits['ssn_start'].dt.date
# DAU for both desktop and touch versions
dau_total = visits.groupby(['session_date']).agg({'user_id':'nunique'}).mean()
print('Daily Active Users:', int(dau_total))
# DAU for different devices
dau_device = visits.groupby(['device', 'session_date']).agg({'user_id':'nunique'})
dau_device.groupby('device').agg({'user_id':'mean'})
# DAU for different devices
desktop_dau = 659
touch_dau = 253
Посмотрим как распределено количество посетителей веб ресурса.
dau_total_hist = visits.groupby('session_date').agg({'user_id':'nunique'})
dau_total_hist['user_id'].hist(bins=50, figsize = (10, 8))
plt.title('Распределение количества посетителей сайта')
plt.xlabel('Количество пользователей, чел')
plt.ylabel('Частота встречаемости значения')
В какие даты пришло рекордное количество посетителей?
dau_total_hist[dau_total_hist['user_id'] > 1500]
dau_total_hist['user_id'].describe()
daily_ssns = visits.groupby('session_date').agg({'user_id':'count'}).mean()
print('Daily sessions:', int(daily_ssns))
visits.pivot_table(index = 'session_date', columns = 'device', values = 'user_id', \
aggfunc = 'nunique').plot(grid = True, figsize = (12, 8))
plt.title('DAU by device')
plt.xlabel('Session date')
plt.ylabel('Users quantity')
В среднем 907 человек используют приложение ежедневно. Обычно у приложения 987 сессий в день. Всплеск активности наблюдался в ноябре 24, 25 в период черной пятницы, тогда сайт посетило рекордное количество человек (3319 п-лей). Видимо, люди активно искали события и скидки. Также, в 2018 году пользователи активизировались в марте, в период детских каникул (весенние каникулы — с 23 марта по 31 марта) и 31 мая в четверг. Видимо, в связи с началом лета и будущими продолжительными выходными.
Активных пользователей больще у компьютерной версии сайта.
Я бы посмотрела среднюю продолжительность сессий на пиках, чтобы в новом году подготовиться к всплеску интереса пользователей. запомним даты: 2018-05-31, 2017-11-24.
# WAU for both desktop and touch versions
wau_total = visits.groupby('session_week').agg({'user_id':'nunique'}).mean()
print('Weekly Active Users:', int(wau_total))
# WAU for different devices
wau_device = visits.groupby(['device', 'session_week']).agg({'user_id':'nunique'})
wau_device.groupby('device').agg({'user_id':'mean'})
# WAU for different devices
desktop_wau = 4208
touch_wau = 1666
weekly_sessions = visits.groupby('session_week').agg({'user_id':'count'}).mean()
print('Weekly Sessions:', int(weekly_sessions))
# MAU for both desktop and touch versions
mau_total = visits.groupby('session_month').agg({'user_id':'nunique'}).mean()
print('Monthly Active Users:', int(mau_total))
# MAU for different devices
mau_device = visits.groupby(['device','session_month']).agg({'user_id':('nunique')})
mau_device.groupby('device').agg({'user_id':'mean'})
# MAU for different devices
desktop_mau = 16724
touch_mau = 6826
В среднем 907 человек используют приложение ежедневно, 5825 еженедельно и 23228 ежемесячно. Около 70% пользователей используют компьютерную версию сайта.
Daily Active Users = 907
Weekly Active Users = 5825
Monthly Active Users = 23228
Теперь мы знаем, сколько пользователей заходит в приложение. Посмотрим, сколько пользователи сидят в приложении. Узанем время 1 сессии и сколько сессий приходится на 1 пользователя ежадневно и за месяц. Сессия — набор действий пользователя, выполняемый от открытия приложения до окончания его использования.
Полезно изучить то, сколько сессий приходится на одного пользователя, например, за месяц. Это хороший показатель регулярности использования приложения. Найти эту метрику просто — разделим число сессий на количество пользователей за период
# creating a table with numbers of sessions and users per month
sessions = visits.groupby(['session_year', 'session_month']).agg({'user_id':['count','nunique']})
#renaming its columns
sessions.columns = ['n_sessions', 'n_users']
sessions['ssns_per_user'] = sessions['n_sessions'] / sessions['n_users']
sessions
print('В среднем приходится {:.2f} сессий на 1 пользователя'.format(sessions.ssns_per_user.mean()))
В месяц один пользователь в среднем проводит в приложении 1 сессию. Не сказать, что продукт используется регулярно. Пользователи редко возвращаются. В ноябре, зимой, марте и мае количество сессий увеличивается.
Узнаем сколько времени пользователи проводят на сайте. Вычислим среднюю продолжительность сессии (ASL) и узнаем сколько длится сессия пользователя в среднем. Посчитаем длину всех сессий и вычислим среднее с учетом спкцифики продукта среднюю продолжительность узнаем по моде, среднему или медиане.
visits['duration_sec'] = (visits['ssn_end'] - visits['ssn_start']).dt.seconds
# let's check the distribution of sessions durations
visits['duration_sec'].hist(bins=50, range=(0, 2000)).set_title('Distribution of sessions durations')
plt.xlabel('duration, sec')
plt.ylabel('frequency')
Распределение длительности пользовательской сесии не похоже на нормальное распределение, кроме того, среднее будет сильно смещено в большую сторону. Узнаем среднюю продолжительность ASL по моде.
print('ASL:', int(visits['duration_sec'].mode()), 'sec')
# visits info for 2017
visits_2017 = visits.query('session_year == 2017')
# visits info for 2018
visits_2018 = visits.query('session_year == 2018')
print('ASL 2017:', int(visits_2017['duration_sec'].mode()), 'sec')
print('ASL 2018:', int(visits_2018['duration_sec'].mode()), 'sec')
2018-05-31, 2017-11-24
visits_24_11 = visits.query('(ssn_start >= datetime(2017, 11, 24, 0, 0)) and (ssn_start <= datetime(2017, 11, 24,23,59))')
visits_05_31 = visits.query('(ssn_start >= datetime(2018, 5, 31, 0, 0)) and (ssn_start <= datetime(2018, 5, 31,23,59))')
extra_visits = pd.concat([visits_24_11, visits_05_31], ignore_index=True)
extra_visits.groupby(['session_date', 'device']).agg({'duration_sec':'median'})
# let's check mode
extra_visits.groupby(['session_date', 'device']).duration_sec.apply(lambda x: x.mode())
В среднем пользователь проводит 1 минуту, изучая сайт Яндекс.Афиша, как на компьютере, так в мобильной версии. Падения средней продолжительности сессии за год не было выявлено. По месяцам медианное значение также остается на одном уровне, увеличиваясь в дни, когда сайтом пользуется большое количество людей в связи с особыми событиями, как черная пятница, празники, каникулы. Судя по медианным значениям для компьютеров и планшетов, мобильная версия работает исправно: сессии чуть короче десктопных.
Вопросы: Успевает ли пользователь за 1 минуту сделать заказ? Те, кто сделал заказ в среднем проводят на сайте столько же времени, что и те, кто посещает сайт в поисках интересных событий? Как отличаются конверсии в продажи у мобильной и десктопной версии?
Посмотрим как менялась средняя продолжительность 1 сессии по месяцам.
asl_monthly = visits.groupby(['device','session_year', 'session_month']).agg({'user_id':'nunique', 'duration_sec':'median'})
asl_monthly
visits.pivot_table(index = 'session_month', columns = 'device', values = 'duration_sec', \
aggfunc = 'median').plot(grid = True, figsize = (12, 8))
plt.title('Average Session Length by device')
plt.xlabel('Session month, month')
plt.ylabel('Session length, min')
asl_device = visits.groupby(['session_year', 'device']).agg({'user_id':'nunique', 'duration_sec':'median'})
asl_device
# visits info for desctop version
visits_dtop = visits.query('device == "desktop"')
print('ASL on desktop version 2018: {} sec'.format(int(visits_dtop['duration_sec'].mode())))
Медианные значения средней продолжительности сессии для десктопной и мобильной версий разнятся. В то время как значение моды остается на прежнем уровне: 1 минута для среднестатистической сессии. Скорее всего те пользователи, кто хочет сделать заказ и более подробный поиск, делают его на компьютере. А быстрый поиск или ознакомление с сайтом в среднем не занимает у пользователей больше минуты как за компьютером, так и за телефоном/планшетом. Это можно проверить воронкой конверсии в продажи для 2 разных версий сайта.
w_sticky_f = dau_total / wau_total*100
m_sticky_f = dau_total / mau_total*100
print('Weekly sticky factor:', int(w_sticky_f), '%', '\n', 'Monthly sticky factor:', int(m_sticky_f), '%', sep='')
Регулярность использования приложения не высокая. 13% пользователей еженедельно возвращаются на сайт. И только 3% пользователей возвращаются в приложение в течение месяца. Популярные игровые приложения имеют показатель в 20-25%. Да, до игровых показателей еще далеко.
Клиенты приходят и уходят, а чем дольше привлечённый пользователь пользуется продуктом, тем выше потенциальная выручка и чем дольше пользователи пользуются сервисом, тем больше аудитории увидит объявления рекламодателя.
Данный показатель отражает реальную картину лояльности клиентов по отношению к бизнесу. Показатель важен, потому что многочисленные исследования уже доказали, что бизнесу дешевле удержать текущих клиентов, нежели привлекать новых.
Проверим Retention rate для нашего сервиса. Интересно, какой процент пользователей, пришедших впервые до сих пор пользуются Яндекс Афишей.
Коэффициент удержания вычислим сначала для всей таблицы, а потом для компьютеров, разбив на когорты по месяцам. а для моб версии по неделям.
CRR =((количество покупателей на конец периода – количество новых покупателей)/количество клиентов на начало периода)100
Коэффициент удержания = ((CEnd – CNew) / CStart)) 100%
# first activity date
# creating a new df 'user_activity' the same as visits but with a column of first activity date
# for each user we are adding his first activity date
first_activity_date = visits.groupby(['user_id'])['ssn_start'].min()
first_activity_date.name = 'first_activity_date'
user_activity = visits.join(first_activity_date,on='user_id')
# weeks when a user was active
user_activity['activity_week'] = pd.to_datetime(user_activity['ssn_start'], unit='d') - \
pd.to_timedelta(user_activity['ssn_start'].dt.dayofweek, unit='d')
# the first activity week
user_activity['first_activity_week'] = pd.to_datetime(user_activity['first_activity_date'], unit='d') - \
pd.to_timedelta(user_activity['first_activity_date'].dt.dayofweek, unit='d')
# numbers of weeks 2nd way of extracting month number
#user_activity['n_first_activity_week'] = user_activity['first_activity_week'].dt.week
#user_activity['n_activity_week'] = user_activity['activity_week'].dt.week
user_activity['cohort_lifetime'] = user_activity['activity_week'] - user_activity['first_activity_week']
user_activity['cohort_lifetime'] = user_activity['cohort_lifetime'] / np.timedelta64(1,'W')
user_activity['cohort_lifetime'] = user_activity['cohort_lifetime'].astype('int')
#2nd way od the same result
#user_activity['n_cohort_lifetime'] = user_activity['n_activity_week'] - user_activity['n_first_activity_week']
Сгруппируем данные по первому посещению(т е по когортам) и по lifetime когорт. Посчитаем для каждой когорты количество активных пользователей на определённую «неделю жизни»:
# adding index with ().reset_index()
cohorts = (user_activity.groupby(['first_activity_week', 'cohort_lifetime']).agg({'user_id':'nunique'})).reset_index()
Чтобы найти Retention Rate, нужно сперва получить число пользователей, изначально бывших в когорте, и на него разделить число пользователей в каждую следующую неделю. Найдём исходное количество пользователей в когорте. Возьмём их число на нулевую неделю:
initial_users_count = cohorts[cohorts['cohort_lifetime'] == 0][['first_activity_week','user_id']]
initial_users_count = initial_users_count.rename(columns={'user_id':'cohort_users'})
#Объединим данные по когортам с исходным количеством пользователей в когорте:
cohorts = cohorts.merge(initial_users_count, on='first_activity_week')
# chohort_users = initial_users (CStart)
# user_id = active users of the week examined (CEnd - CNew)
cohorts['retention'] = cohorts['user_id']/cohorts['cohort_users']*100
Построим кривую изменения коэффициента удержания.
cohorts.groupby('cohort_lifetime')['retention'].mean().plot(figsize=(12, 8))
plt.title('Retention curve')
plt.axhline(y = 50, label = '50 % Retention', color = 'red', linestyle = '--')
plt.xlabel('Weeks Since First Use, week')
plt.ylabel('Persentage of Users Returning, %')
plt.legend()
В начале изучаемого периода в первые месяцы уходило 70% клиентов, но уже к концу изучаемого периода Retention Rate поднялся выше 50%. Это хороший показатель того, что сайт нравится пользователям и уже половинa пользователей вернулась. За этой метрикой очень важно следить, так как она чувствительна к изменениям пользовательского одобрения и хорошо показывает успешность и провал нововведений.
Таблица user_activity уже готова для когортного анализа, осталось только вычислить lifetime в месяцах.
user_activity['first_activity_month'] = user_activity['first_activity_date'].astype('datetime64[M]')
user_activity['activity_month'] = user_activity['session_date'].astype('datetime64[M]')
user_activity['month_lifetime'] = user_activity['activity_month'] - user_activity['first_activity_month']
user_activity['month_lifetime'] = user_activity['month_lifetime'] / np.timedelta64(1,'M')
user_activity['month_lifetime'] = user_activity['month_lifetime'].round().astype('int')
user_activity.head()
Сгруппируем данные по первому посещению(т е по когортам) и по lifetime когорт. Посчитаем для каждой когорты количество активных пользователей на определённый «месяц жизни»:
month_cohorts = user_activity.groupby(['first_activity_month', 'month_lifetime']).agg({'user_id':'nunique'}).reset_index()
month_cohorts = month_cohorts.rename(columns={'user_id':'monthly_users'})
Чтобы найти Retention Rate, нужно сперва получить число пользователей, изначально бывших в когорте, и на него разделить число пользователей в каждую следующую неделю. Найдём исходное количество пользователей в когорте. Возьмём их число на нулевой месяц:
# number of people from 1st month of existence for each date
initial_users_month = month_cohorts[month_cohorts['month_lifetime'] == 0][['first_activity_month',\
'monthly_users']].reset_index()
initial_users_month = initial_users_month.rename(columns={'monthly_users':'cohort_users'})
initial_users_month
#Объединим данные по когортам с исходным количеством пользователей в когорте:
month_cohorts = month_cohorts.merge(initial_users_month, on='first_activity_month')
month_cohorts
month_cohorts['retention'] = month_cohorts['monthly_users']/month_cohorts['cohort_users']*100
month_cohorts.head()
month_retention_pivot = month_cohorts.pivot_table(index='first_activity_month',\
columns='month_lifetime',values='retention',aggfunc='sum').drop([0], axis=1)
# drop for deleting the column of 100 %
display(month_retention_pivot.fillna(''))
month_cohorts.groupby('month_lifetime')['retention'].mean().plot(figsize=(12, 8))
plt.title('Retention curve')
plt.axhline(y = 50, label = '50 % Retention', color = 'red', linestyle = '--')
plt.xlabel('Months Since First Use, month')
plt.ylabel('Persentage of Users Returning, %')
plt.legend()
sns.set(style='white')
plt.figure(figsize=(13, 9))
plt.title('Cohorts: User Retention')
sns.heatmap(month_retention_pivot, annot=True, fmt='.1f', linewidths=1, linecolor='gray');
Когда люди начинают покупать? Сколько раз покупают за период? Какой средний чек? Сколько денег приносят? (LTV)
Чтобы ответить на эти вопросы приступим к когортному анализу. Разделим пользователей по дате первого обращения на сайт.
Таблица visits (лог сервера с информацией о посещениях сайта): user_id — уникальный идентификатор пользователя device — категория устройства пользователя ssn_start — дата и время начала сессии ssn_end — дата и время окончания сессии source_id — идентификатор рекламного источника, из которого пришел пользователь
Таблица orders (информация о заказах): user_id — уникальный id пользователя, который сделал заказ order_date — дата и время заказа revenue — выручка Яндекс.Афиши с этого заказа
Есть данные о каждом покупателе интернет-магазина: дата и стоимость его заказа. Задача: понять, как по месяцам изменяется количество пользователей, совершивших покупку впервые. Когда выяснится, растёт число «новичков» или падает, можно будет понять, кто прибыльнее — старые покупатели или новые.
Июньский период не был изучен целиком, поэтому удалим его.
# deleting june period as not studied
orders = orders.query('order_date < datetime(2018,6,1)')
<> Исследуем сколько времени проходит между первым посещением и первой покупкой. Построим гистограмму, найдем среднее и медиану.
# creating a column with first activity date
first_activity_date_id = visits.groupby('user_id')['ssn_start'].min()
first_activity_date_id.name = 'first_ssn_date'
first_order_date_id = orders.groupby('user_id')['order_date'].min()
# let's rename our series object so in the futere it's ready to be added to the main df
first_order_date_id.name = 'first_order_date'
# df with first session and first order
history = pd.concat([first_activity_date_id, first_order_date_id], axis=1).dropna(axis=0)
display(history.head())
history['wait_time'] = history['first_order_date'] - history['first_ssn_date']
history['wait_time'].describe()
fig = px.histogram(history, x="wait_time", marginal="box", title = 'Распределение временного периода от первого посещения до покупки',\
labels={
"count": "Количество",
"wait_time": "Сколько дней проходит до первой покупки"
})
fig.show()
+ Из 228169 посещений было сделано 36522 покупки.
+ То есть чаще всего покупка не совершается, либо совершается в тот же день (в 50% случаев) или в 25% случаях проходит 2 дня. Совсем редко пользователь не совершает первую покупку до года.
+ Среднее значение составляет 16 дней, но нужно понимать, что оно сильно смещено вправо (завышено) из-за длинного хвоста нетипичных значений
# let's merge the new column and the main df
orders = orders.join(first_order_date_id, on='user_id')
display(orders.head())
orders['first_order_month'] = orders['first_order_date'].astype('datetime64[M]')
orders['order_month'] = orders['order_date'].astype('datetime64[M]')
Когортой сделаем столбец first_order_month — месяц, в котором был сделан первый заказ. Сгруппируем данные по этому столбцу и оценим показатели каждой когорты.
cohort_grouped = orders.groupby('first_order_month')\
.agg({'user_id':'nunique','revenue':'sum'})
cohort_grouped
cohort_grouped['revenue'].plot(figsize=(10, 6))
plt.title('Кривая месячной выручки')
plt.xlabel('Месяц исследуемого периода')
plt.ylabel('Месячная выручка, у.е.')
plt.legend()
cohort_grouped['user_id'].plot(figsize=(10, 6))
plt.title('Кривая количества заказов на сайте в месяц')
plt.xlabel('Месяц исследуемого периода')
plt.ylabel('Количество пользователей сайта, чел.')
plt.legend()
После декабря выручка по каждой следующей когорте меньше предыдущей. Это логично, ведь более старые когорты существуют дольше, и пользователи из них совершали заказы в течение бóльшего времени.
А ещё: количество пользователей, составляющих каждую когорту, возрасло с момента выгрузки данных: за зиму появилось много новых пользователей. Наблюдаем положительную тенденцию роста аудитории сайта. Июнь еще не был изучен.
Отследим, как изменяется количество покупающих пользователей и выручка внутри одной когорты. Это позволит понять, сколько людей продолжают покупать в следующих месяцах после совершения первой покупки.
Сколько раз покупают за период?
# quantity of orders per month
orders.pivot_table(index='first_order_month',
columns='order_month',
values='user_id',
aggfunc='count').fillna('')
# quantity of unique users who still order from time to time within each chohort
orders.pivot_table(index='first_order_month',
columns='order_month',
values='user_id',
aggfunc='nunique').fillna('')
Какой средний чек?
Ещё одна задача когортного анализа: понять, как с течением времени изменяется средний чек — сумма покупок, делённая на число покупателей. Так узнаем, увеличивается или уменьшается средний чек покупателей, которые продолжают делать заказы в интернет-магазине.
# total revenue and number of members for each cohort
orders_grouped_by_cohorts = orders.groupby(['first_order_month','order_month']).agg({'revenue':'sum','user_id':'nunique'})
# orders_grouped_by_cohorts
Найдём средний чек покупателя revenue_per_user.
Средний чек - это средняя выручка в месяц на покупателя, а не на отдельную транзакцию.
# revenue_per_user
orders_grouped_by_cohorts['revenue_per_user'] = orders_grouped_by_cohorts['revenue'] / \
orders_grouped_by_cohorts['user_id']
# pivot table with average bill per user
orders_grouped_by_cohorts.pivot_table(index='first_order_month',columns='order_month',\
values='revenue_per_user',aggfunc='mean').fillna('')
orders_grouped_by_cohorts.pivot_table(index='first_order_month',columns='order_month',\
values='revenue_per_user',aggfunc='mean').plot(figsize=(18, 5))
orders_grouped_by_cohorts['revenue_per_user'].describe()
print(('Средний чек покупателя составляет {:.2f} y.e.'\
.format(orders_grouped_by_cohorts['revenue_per_user'].median())))
Как изменяется средний чек от месяца к месяцу? В каком месяце самый высокий средний чек?
Средний чек не превышает 20 у.е и составляет 11 у.е. Обычно его значения колеблятся от 4 до 18 у.е. Самый высокий средний чек зафиксирован в сентябре у июньской когорты, тогда он составил 19у.е.
half_orders = orders.query('order_month >= datetime(2017,12,1) and first_order_date < datetime(2018,1,1)')
half_orders_grouped_by_cohorts = (half_orders.groupby(['first_order_month','order_month'])
.agg({'revenue':'sum','user_id':'nunique'}))
half_orders_grouped_by_cohorts['revenue_per_user'] = half_orders_grouped_by_cohorts['revenue'] / \
half_orders_grouped_by_cohorts['user_id']
half_orders_grouped_by_cohorts.pivot_table(index='first_order_month',columns='order_month',\
values='revenue_per_user', aggfunc='mean').fillna('')
Средний чек не был занижен более новыми когортами.
# adding a columnn of life time for cohorts
orders_grouped_by_cohorts = orders_grouped_by_cohorts.reset_index()
orders_grouped_by_cohorts['cohort_lifetime'] = orders_grouped_by_cohorts['order_month'] - \
orders_grouped_by_cohorts['first_order_month']
orders_grouped_by_cohorts['cohort_lifetime'].head()
# let's change day's amount on month's amount
orders_grouped_by_cohorts['cohort_lifetime'] = orders_grouped_by_cohorts['cohort_lifetime'] / np.timedelta64(1,'M')
# amount value must be integer
orders_grouped_by_cohorts['cohort_lifetime'] = orders_grouped_by_cohorts['cohort_lifetime'].round().astype('int')
# leaving only year and month
orders_grouped_by_cohorts['first_order_month'] = orders_grouped_by_cohorts['first_order_month'].dt.strftime('%Y-%m')
revenue_per_user_pivot = (orders_grouped_by_cohorts.pivot_table(index='first_order_month',
columns='cohort_lifetime',values='revenue_per_user',aggfunc='mean')
)
revenue_per_user_pivot.fillna('')
Тепловая карта изменения среднего чека покупателей <> Back to Summary
plt.figure(figsize=(13, 9))
plt.title('Средний чек покупателей')
sns.heatmap(revenue_per_user_pivot, annot=True, fmt='.1f', linewidths=1, linecolor='gray')
plt.xlabel('Когорта')
plt.ylabel('Жизненный период когорты')
Во всех когортах средний чек пользователей возрастает на следующий месяц после месяца первой покупки. Средний чек повсеместно растёт с течением времени. С начала периода средний чек вырос с 4 у.е до 7,5. За редким исключением встречаются чеки до 20 у.е. на покупателя.
# определяем источник пользователя
sources = visits.sort_values(by = ['user_id', 'ssn_start']).groupby('user_id').agg({'source_id': 'first'})
# соединяем первые покупки и источники
payers = orders.groupby('user_id').agg({'order_date': 'min'}).merge(sources, on = 'user_id').reset_index()
payers = payers.rename(columns = {'order_date': 'first_buy'})
# считаем ltv
ltv = payers.merge(orders, on = 'user_id')
ltv['lifetime'] = ((ltv['order_date'] - ltv['first_buy']) / np.timedelta64(1,'M')).round().astype(int)
orders_lifetime = ltv.pivot_table(index = 'source_id', columns = 'lifetime', values = 'revenue', aggfunc = 'count').fillna(0)
buyers_lifetime = ltv.pivot_table(index = 'source_id', columns = 'lifetime', values = 'user_id', aggfunc = 'nunique').fillna(0)
ltv = ltv.pivot_table(index = 'source_id', columns = 'lifetime', values = 'revenue', aggfunc = 'sum').fillna(0)
ltv = ltv.cumsum(axis = 1)
# добавляем размеры когорт и делим на них
# div(ltv['cohort_size'], axis = 0) берет столбец cohort_size и делит на него каждый столбец фрейма
cohort_sizes = payers.groupby('source_id').agg({'user_id': 'nunique'}).rename(columns = {'user_id': 'cohort_size'})
ltv = ltv.merge(cohort_sizes, on = 'source_id')
ltv = ltv.div(ltv['cohort_size'], axis = 0).drop(columns = 'cohort_size')
display(ltv)
visits.groupby('source_id')['user_id'].nunique()
costs.groupby('source_id')['costs'].sum()
1, 2, 5 и 9 источники привлекают наиболее платежеспособных клиентов. От источников 6 и 7 пришло меньше 50 человек, но это были бесплатные источники трафика, как браузеры, например или рекомендации друзей.
# Посчитаем количество новых покупателей (n_buyers) за каждый месяц:
cohort_quantity = orders.groupby('first_order_month').agg({'user_id': 'nunique'}).reset_index()
cohort_quantity.columns = ['first_order_month', 'n_buyers']
print(cohort_sizes.head())
# Сгруппируем таблицу заказов по месяцу первой покупки и месяцу каждого заказа и сложим выручку.
# Сбросим индекс методом reset_index():
cohort_revenue = orders.groupby(['first_order_month','order_month']).agg({'revenue': 'sum'}).reset_index()
cohort_revenue.head()
Из таблицы cohort_revenue видно какую выручку принесла каждая когорта за обределенный месяц. Добавим в эту таблицу данные о том, сколько людей первый раз совершили покупку в каждый месяц:
report = pd.merge(cohort_quantity, cohort_revenue, on='first_order_month')
report.head()
LTV считают не из выручки, а из валовой прибыли. Значит, нужно найти валовую прибыль, умножив выручку на маржинальность. Будейм считать, что у нас маржинальность равна 100%, так как в начальных условиях она не была дана. Так как LTV — относительный показатель, то его удобнее смотреть по «возрастным» когортам, где в столбцах — не месяц заказа, а возраст (age) когорты.
report['age'] = (report['order_month'] - report['first_order_month']) / np.timedelta64(1, 'M')
report['age'] = report['age'].round().astype('int')
# margin_rate = 1 so gross = revenue
report['ltv'] = report['revenue'] / report['n_buyers']
output = report.pivot_table(
index='first_order_month',
columns='age',
values='ltv',
aggfunc='mean')
output = output.cumsum(axis = 1)
output.fillna('')
plt.figure(figsize=(13, 9))
sns.heatmap(output, annot=True, fmt='.1f', linewidths=1, linecolor='gray')
plt.title('LTV когорт по дате первой покупки')
plt.xlabel('Жизненный период когорты')
plt.ylabel('Когорта')
# Посчитаем итоговый LTV первой когорты. Сложим их LTV по месяцам:
ltv_201706 = output.loc['2017-06-01', 11]
print('Итоговый LTV первой когорты:', ltv_201706)
ltv_6th_cohort = output[5].mean()
print('Итоговый LTV 6ой когорты:', ltv_6th_cohort)
LTV первой когорты составил 11.87 у.е.
halff_orders = orders.query('order_date < datetime(2017,12,1)') # df of only 6 months
# выручка за первые 6 месяцев
half_revenue = halff_orders['revenue'].sum()
# unique users of the first 6 months
half_users_unique = halff_orders['user_id'].nunique()
half_year_ltv = half_revenue / half_users_unique
half_year_ltv
print('Прмерный LTV за первые 6 месяцев {:.2f} у.е.'.format(half_year_ltv))
Сколько денег потратили на каждый источник по месяцам и всего?
# let's add a column only with a month and a year
costs['month'] = costs['date'].dt.strftime('%Y-%m')
detailed_costs = costs.pivot_table(index='source_id', columns='month', values='costs', aggfunc='sum', margins=True)
detailed_costs
plt.figure(figsize=(13, 9))
plt.title('Расходы на рекламу в у.е.')
sns.heatmap(detailed_costs, annot=True, fmt='.1f', linewidths=1, linecolor='gray', \
vmin=0, vmax=50000)
plt.xlabel('Месяц, год')
plt.ylabel('Идентификатор источника')
Большинство затрат на маркетинг приходится на источник с идентификатором 3, дальше идут по убыванию 4, 5, 2 и 1. На 9 и 10 источники в год тратится суммарно 4% всего бюджета, т.е. намного меньше, чем на остальные.
<> Сколько стоило привлечение одного покупателя из каждого источника?
# считаем САС
cac = costs.pivot_table(index='source_id', values = 'costs', \
aggfunc='sum').merge(cohort_sizes, on = 'source_id')
cac['cac'] = cac['costs'] / cac['cohort_size']
display(cac)
Поделим все расходы на маркетинг за полгода с июня 2017 по ноябрь 2017 включительно на всех пользователей привлеченных за этот период.
half_costs = costs.query('date < datetime(2017,12,1)') # df of only 6 months
total_costs_6months = half_costs['costs'].sum()
print('На маркетинг было потрачено', total_costs_6months, 'y.e.')
total_users_aqiuered = halff_orders['user_id'].nunique()
print('Пользователей привлечено', total_users_aqiuered, 'человек.')
half_year_cac = total_costs_6months / total_users_aqiuered
print('Примерное значение САС равно {:.2f} y.e на пользователя.'.format(half_year_cac))
Стоимость привлечения пользователя приблизительно равна 9 у.е., что превышает LTV равное 6 у.е. Это значит, что затраты на маркетинг окупаются.
<> На сколько окупились расходы? (ROI) to Summary
# считаем ROI
roi = ltv.merge(cac[['cac']], on = 'source_id')
roi = roi.div(roi['cac'], axis = 0).drop(columns = 'cac')
display(roi)
roi.T.plot(figsize=(15, 5))
plt.axhline(y = 1, label = 'Уровень окупаемости', color = 'red', linestyle = '--')
plt.title('ROI с разбивкой по источникам привлечения')
plt.xlabel('Lifetime')
plt.legend()
plt.show()
costs.pivot_table(index='source_id', values='costs', aggfunc='sum', margins=True)
Видно, что 1-й источник пересек уровень окупаемости почти сразу, источники 2,5 и 9 задержались на полгода, остальные вообще не окупились.
Если еще раз посмотреть на затраты по источникам, то видно, что 10 и 3 источники показывают низкую окупаемость. 4 источник тоже можно поставить под вопрос.
Самые дорогие источники: 3 и 4. 5, 2, 1 - средней стоимости. 9 и 10 самые дешевые. А за 6, 7, 8 источницки ничего не заплатили. Возможно, это разные браузеры, через которые пользователи заходили на сайт.
# определяем устройство пользователя
# допустим, что первое использованное устройство более предпочтительное для пользователя
devices = visits.sort_values(by = ['user_id', 'ssn_start']).groupby('user_id').agg({'device': 'first'})
orders_device = devices.merge(orders, on = 'user_id')
costs['costs'].sum()
orders['revenue'].sum()
# смотрим LTV CAC ROI по дейвайсам
# соединяем первые покупки источники и устройства
# 1st buy and source
device_payers = orders_device.groupby(['user_id', 'device']).agg({'order_date': 'min'}).merge(sources, on = 'user_id').reset_index()
device_payers = device_payers.rename(columns = {'order_date': 'first_buy'})
device_ltv = device_payers.merge(orders_device, on = 'user_id').reset_index()
device_ltv['lifetime'] = ((device_ltv['order_date'] - device_ltv['first_buy']) / np.timedelta64(1,'M')).round().astype(int)
orders_new = device_ltv.copy()
# считаем ltv
device_buyers_lifetime = device_ltv.pivot_table(index =('source_id', 'device'), columns = 'lifetime', values = 'user_id', aggfunc = 'nunique').fillna(0)
device_ltv = device_ltv.pivot_table(index = ('source_id', 'device'), columns = 'lifetime', values = 'revenue', aggfunc = 'sum', margins=True).fillna(0)
device_ltv = device_ltv.cumsum(axis = 1)
device_ltv
# добавляем размеры когорт и делим на них
# div(ltv['cohort_size'], axis = 0) берет столбец cohort_size и делит на него каждый столбец фрейма
device_cohort_sizes = orders_new.groupby(['source_id', 'device']).agg({'user_id': 'nunique'}).rename(columns = {'user_id': 'cohort_size'})
device_ltv = device_ltv.merge(device_cohort_sizes, on = ['source_id', 'device'])
device_ltv = device_ltv.div(device_ltv['cohort_size'], axis = 0).drop(columns = 'cohort_size')
display(device_ltv)
# считаем САС
cac = costs.groupby('source_id').agg({'costs': 'sum'}).merge(cohort_sizes, on = 'source_id')
cac['cac'] = cac['costs'] / cac['cohort_size']
display(cac)
ttl_costs = (costs.groupby('source_id')['costs'].sum()).reset_index()
ttl_costs
orders_new = orders_new.merge(cohort_sizes, on = 'source_id')
orders_new = orders_new.merge(ttl_costs, on = 'source_id')
orders_new.head()
piv_1 = orders_new.pivot_table(index=('source_id', 'device'),\
values=('costs', 'cohort_size'), fill_value=0)
piv_1
piv_2 = orders_new.pivot_table(index=('source_id', 'device'),\
values=('user_id', 'cohort_size', 'costs'), aggfunc=({'user_id':'nunique'}), fill_value=0)
piv_2 = piv_2.rename(columns={'user_id':'users'})
#piv.index.get_level_values(2)
piv_2
device_cac = piv_2.merge(piv_1, on=('source_id', 'device'))
device_cac
device_cac['part'] = device_cac['users'] / device_cac['cohort_size']
device_cac
device_cac['device_cost'] = device_cac['part'] * device_cac['costs']
device_cac['cac'] = device_cac['device_cost'] / device_cac['users']
device_cac
# считаем ROI
device_roi = device_ltv.merge(device_cac[['cac']], on = ('source_id', 'device'))
device_roi = device_roi.div(device_roi['cac'], axis = 0).drop(columns = 'cac')
device_roi
device_roi.T.plot(figsize=(25, 20))
plt.axhline(y = 1, label = 'Уровень окупаемости', color = 'red', linestyle = '--')
plt.title('ROI с разбивкой по источникам привлечения', fontsize = 25)
plt.xlabel('Lifetime', fontsize = 30)
plt.ylabel('ROI', fontsize = 20)
plt.legend(fontsize = 20)
plt.show()
В целом маркетинг для компьютерной версии сайта быстрее окупается. Не все источники одинаково хороши для 2 версий сайта: 2 и 3 источники слабо окупаются мобильной версией.
# всего посетителей: users
visits['user_id'].shape[0]
visits.groupby('device')['user_id'].count()
# уникальных посетителей: unique_users
visits['user_id'].nunique()
visits.groupby('device')['user_id'].nunique()
# покупок purchases
orders.shape[0]
orders_device.groupby('device')['user_id'].count()
# уникальных покупателей unique_payers
orders['user_id'].nunique()
orders_device.groupby('device')['user_id'].nunique()
# повторных покупок
#quantity of purchases
purchase = orders.groupby('user_id')['order_date'].count().reset_index()
purchase.query('order_date > 1').shape[0]
purchase = orders_device.groupby(['device', 'user_id'])['order_date'].count().reset_index()
purchase = purchase.query('order_date > 1')
purchase.groupby('device')['user_id'].count()
steps=['users', 'unique_users', 'purchases', 'unique_payers', 'repeated_purchases']
desktop_figures = [262567, 164523, 40926, 29210, 5002]
touch_figures = [96833, 71345, 9488, 7312, 1290]
fig = go.Figure(data=[
go.Bar(name='Desktop', x=steps, y=desktop_figures,
text=desktop_figures,
textposition='auto'),
go.Bar(name='Touch', x=steps, y=touch_figures,
text=touch_figures,
textposition='auto')
])
# Change the bar mode
fig.update_layout(barmode='group')
fig.update_layout(title_text='Воронка конверсии пользователей разных устройств в повторные покупки')
fig.show()
62% сессий за год было совершено новыми пользователями и 38% сессий составили повторные заходы знакомых с продуктом пользователей. 25% пользователей совершают покупки с компьютера и только 13% пользователей с мобильной версией. Похоже, что клиенты более склонны делать заказы с десктопной версии сайта. Поэтому и затраты на маркетинг там быстрее окупаются.
steps=['users', 'purchases', 'repeated_purchases']
total_figures = [359400, 50414, 6292]
unique_figures = [228169, 36522, 6292]
fig = go.Figure(data=[
go.Bar(name='Total', x=steps, y=total_figures,
text=total_figures,
textposition='auto'),
go.Bar(name='Unique', x=steps, y=unique_figures,
text=unique_figures,
textposition='auto')
])
# Change the bar mode
fig.update_layout(barmode='group')
fig.update_layout(title_text='Воронка конверсии пользователей в повторные покупки')
fig.show()
16% пришедших на сайт совершают покупки и только 2.7% людей совершают повторные покупки.
(Изучаемый период не включает июнь 2018 года за неимением полных данных)
За изученный период было выявлено несколько дат со значительным всплеском активности на сайте. Такие даты можно использовать для продвижения продукта. Например, черная пятница, школьные каникулы, начало лета провоцировали повышенный интерес к сайту.
Настоящая тактика по привлечению пользователей работает, это видно по метрике Retention Rate (лояльность клиента), которая показывает рост интереса к нашему ресурсу. 50% пользователей вернулись за последние 2 недели до конца изучаемого периода.
Однако, мы имеем недостаточно высокие Sticky Factor и ASL, сейчас средняя продолжительность сессии всего 60 сек, а ежемесячный Sticky Factor всего 3%. Я бы старалась увеличить продолжительность сессий и необходимость сайта для пользователя.
Если есть задача работать на удержание клиентов, и на поддержание лояльности, то пользовательские когорты июня 2017 года и октября 2017 продолжают составлять сравнительно большую долю покупателей, можно обратить на этих пользователей внимание. Средний чек колебался от 7,5 до 20 на конец изучаемого периода и составлял 11 у.е. Heatmap
В целом, пользователи больше проводят времени на сайте и чаще покупают, когда используют компьютерную версию сайта. См. Воронки конверсий. Воронка конверсии пользователей разных устройств в повторные покупки При чем, если пользователь заходит на сайт впервые, то в 16% случаев он сделает покупку.
Маркетинговые затраты на 6 месяце окупились. LTV(9 y.e.) > CAC(6 y.e.).
ROI с разбивкой на устройства показал, что маркетинговые затраты быстрее окупаются именно для десктопной версии. Если есть возможность, я бы скорректировала расходы на рекламу отталкиваясь именно от версии сайта, на которую направлено продвижение. Это еще обусловлено тем, что люди в 80 % случаев делают заказы с компьютера.
Важно отметить, что из всех посетителей лишь 16 % делают заказ. Из них 25% пользователей совершают покупки с компьютера и только 13% пользователей с мобильной версии.
Повторные покупки совершают только 2.7% людей.
Источники трафика
Продуктивные:
1, 2, 5 и 9 источники привлекают наиболее платежеспособных клиентов. ( видно по LTV с разбивкой на источники) LTV graph
1-й источник пересек уровень окупаемости почти сразу, источники 2,5 и 9 задержались на полгода, остальные вообще не окупились.
Если еще раз посмотреть на затраты по источникам, то видно, что 10 и 3 источники показывают низкую окупаемость. 3 источник нужно поставить под вопрос, тк он самый дорогой и наименее продуктивный, судя по ROI. ROI
НЕ оправдавшие уровень затрат ресурсы: 3, 10 источники.
Также, не все источники одинаково хороши для 2 версий сайта.
Ресурсы действенные для мобильной версии: 1, 4
Ресурсы действенные для компьютерной версии: 1, 2, 4, 5, 9